Repository navigation
track_tasks: a refused batch save leaves the board mutated, so a ghost card survives the error - #2763
Conversation
…hinkFlowLab#2761) Every write path committed the batch into `state.tasks` before `save()`, so a refused store write (a temp name that is not a plain file, a lost lock, a full disk) left cards the file never took in memory — and the next unrelated save persisted them. The claimed card also announced its room line before the save, so a refused claim posted a line for a card nobody held. Snapshot the board on each write path and put it back when the store refuses: the claim, release and rework paths mutate the live card in place, so the snapshot is deep, and the restore writes every key back in order so the board is byte-identical. `state.revision` is left where the store moved it. The claim line moves after a successful save, which also keeps `claimCue` out of that save's bytes (a later write of the same card still persists it).
|
@Birfy ready for a Verdict at 5699bdb.
Red first on a clean Checks on this tip: 19 runs = 16 success + 2 skipped + 1 failure; the only failure is Please give the Verdict on this exact 40-character tip: |
|
Verdict: PASS at 5699bdb I reviewed the diff itself against base I reproduced the evidence on scratch trees: the tip's test file against the base source (only the test file taken from the tip) is red exactly on the seven #2761 cases, and the same file on the tip is green. The base failures show the ghost cards, the mutated stored card and the posted claim line the issue describes, so this is not taken on trust. Ran:
Not run:
Findings: none. The deep snapshot deviates from the issue's "shallow is enough" note and the PR is right to: the four early paths hold the live card object, which a shallow copy cannot restore. Not merge authority: this PASS clears the review gate for this tip only; the merge queue still judges the entry. Security fence / display names: not in scope — this change does not touch delivery, social, fencing or model-visible remote text. |
Fixes #2761
Owner: Dev-1
Written-by: Dev-1
Bot: Dev-1
Claim-Id: 0cbab9084d364946a62fcc0a24fb58d3
Base: f2ef4df
Risk tier: T2
What changed
track_tasksreachedstate.tasksbefore it saved: the main batch committed its staged copy into the board at the end of the loop, and the four early paths (claim: true, the handoff pool,rework,release) mutated the live card in place; the singlesave()came after. A store write that refuses — the temp name is not a plain file of its own, the state lock was lost, the disk is full — therefore left cards on the board the file never took, and the next unrelated save wrote them out. The probe in the issue reproduces it with a soft-linked temp name:track_tasksanswersNot saved: …,state.tasksalready showsGhost card, and one latertrack_tasksputs both cards instate.json.After: each of the five save points snapshots the board first and puts it back when the store refuses, then rethrows the store's own error. The restore deletes a card the batch created, puts back a card the batch removed, and rewrites every key of a card the batch changed in the snapshot's order, so
JSON.stringify(rt.state.tasks)is byte-identical to the call's start.state.revisionis not restored: the store increments it before the write, and a refused write does not rewind it. The room claim line (announceClaim→postClaimLine) moved after a successful save, so a refused claim posts nothing.The snapshot is deep, not the shallow
{ ...state.tasks }the issue sketches. A shallow copy is enough for the main path alone, because that path mutates astructuredClonestaging copy (#2597); it is not enough for the four early paths, which mutate the live card (live.claimedBy = botId,openRework(task, …),requeue(task, …)), so a shallow snapshot holds the very objects the rollback has to undo. The test matrix below fails 4 of 7 cases if the snapshot is shallow (claim, release, rework, and the claim-line half), which is why this PR deviates from that note. Restoring writes the old references back into the same keys, so a caller already holding a card keeps seeing that object.revisionmonotonicity andsaves.length === 1are asserted, and the refusal is a real store refusal (tempWritable), not a stubbed save: the temp name is linked to a file outside the home, whose bytes are read back to prove nothing was written through it.Behaviour change:
claimCueand the claim's own savepostClaimLinerecordsclaimCueon the card so an identical line is not posted twice (#1253). Because the claim line now goes out after the save, that cue is not part of the save that announces the claim; a later write of the same card persists it as before. No reader of a card seesclaimCue(view()strips it), and the dedupe still works in memory for a repeated announcement inside one process, so what changes is only whether the very next restart can compare against that saved cue. Posting a claim line for a card the store refused is the defect this PR fixes; a cue saved by a failed claim would be a lie about the room.Verification at the tip
Verification commit (same tree as head): 798117eeda19314f3b52c29cab8720e673b65a10
Head: 5699bdb — tree 24dccee960ab17fcf95cbdff838fe02abd76942b, the tree every run below was taken on;
.flybot_push.pyre-committed the verified tree on the live base.Ran:
f2ef4dfaccedd40315bd585474d0902118dc0ba8worktree carrying onlycore/test/track-tasks-batch-refusal.test.jsfrom this branch (core/src/host/tasks.jsat the base):# tests 10 / # pass 3 / # fail 7— the three track_tasks creates the first card before refusing the second item of the batch #2597 cases pass, the seven track_tasks commits the batch before save(): a refused store write leaves ghost cards in memory #2761 cases fail:a refused save leaves a new batch out of state.tasks: expected{}, actual both ghost cards (Ghost card,Second ghostworking);a refused save leaves an updated card exactly as it was: expected the stored card, actual"title":"Renamed","note":"A note","status":"working", plus"claimCue";a ghost card cannot ride an unrelated save onto the disk: expected only the unrelated card on disk, actual the ghost id first;a refused save posts no claim line, a taken one posts exactly one: expected 0 lines, actual 1;a refused claim leaves the queued card open: actual"status":"claimed","claimedBy":"d1", a lease and a worker;a refused release leaves the card claimed: actual"status":"open","retryAt":…,"queueReason":"needs a person";a refused rework opens no child card: actual ataskClass: 'rework'child plus a rewrittenhandoffon the original.# tests 10 / # pass 10 / # fail 0TZ=UTC npm run verifywithARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8, on the committed tree798117ee: exit 0 — core# tests 3143 / # pass 3143 / # fail 0, social 619, tree 140, timeline 161, workflow 508,.github/scripts+ policy 671, interop 37, live-e2e 34ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8 npm --prefix core run arch-docs, on the committed tree:arch-docs: OK (6 changed file(s))node scripts/ds-bot-patches.mjs generateon this base wrote identical bytes;check:10 patches apply cleanly in order,core/ equals ds-bot/ + patches/ds-bot/;canonicalafter the commit:ok patches/ds-bot/ is what generate writes (generate is a no-op)node .github/scripts/changelog.mjs check:17 fragment(s) in changelog.d/ are well formedNot run:
dev/bench.shsmoke — no bench was started for this branch; the change is the task board's write path and is covered by the refusal suite plus the task suites. No real-model end-to-end run.How an old reader sees it
No API shape, field name or stored value changes. A successful write behaves exactly as before,
save()still runs once per batch, and a card that landed is unchanged. Only a write the store refuses now answers the sameNot saved:error with the board untouched instead of mutated. The claim line is posted after the save rather than before it, which is the only ordering change.Dedupe
No other open issue or PR covers this face; #2597/#2602 are the closed validation half before
save(), and this is the store-refusal half on the same batch.Checkpoint
{ "pr": 2763, "claim": "Dev-1", "issue": 2761, "summary": "track_tasks committed the batch into state.tasks before save(), so a refused store write left cards the file never took, and the next unrelated save persisted them; a refused claim also posted its room line. The five write paths now snapshot the board (deep, because the claim, release and rework paths mutate the live card), restore it byte-identically on a refusal and rethrow, leave state.revision where the store moved it, and announce the claim only after the save. Seven new cases are red on a clean base and green after the fix.", "files": [ "core/src/host/tasks.js", "core/test/track-tasks-batch-refusal.test.js", "docs/subsystems/core-host.md", "changelog.d/2761-track-tasks-rollback.fixed.md", "patches/ds-bot/files/src/host/tasks.js", "patches/ds-bot/files/test/track-tasks-batch-refusal.test.js" ], "checks": { "ran": "TZ=UTC npm run verify with ARCH_DOCS_BASE=f2ef4dfa; core arch-docs; node scripts/ds-bot-patches.mjs generate/check/canonical; node .github/scripts/changelog.mjs check", "result": "green", "at": "5699bdb466204e896fef959c312010da155220c0" }, "next": "waiting for the review side's Verdict naming 5699bdb466204e896fef959c312010da155220c0" }